// License Notification
//======================================================
// Copyright (c) 2010 Channel Intelligence Inc.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// ======================================================




using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using CIP4.Common;
using CIP4.Item;

namespace CIP4.Serializer
{
    public abstract class SerializerBuilderBaseType
    {
        public static Type GetActualType(Type t)
        {
            Type actualType = t;
            if (t.IsArray)
                actualType = t.GetElementType();

            if (actualType == null)
                throw new ApplicationException("Unexpected null actual type");

            if (Nullable.GetUnderlyingType(actualType) != null)
                actualType = Nullable.GetUnderlyingType(actualType);

            if (actualType.IsGenericType)
            {
                Type[] types = actualType.GetGenericArguments();
                actualType = types[types.Length - 1];
            }
            return actualType;
        }

        public TypeContextType TheType { get; set; }

        public Dictionary<string, TypeContextType> UsedTypes { get; set; }

        protected Dictionary<string, bool> TypeInclusions = new Dictionary<string, bool>();

        protected Dictionary<string, bool> PropertyInclusions = new Dictionary<string, bool>();

        protected PropertyInfo VersionProperty
        {
            get
            {
                return TheType.VersionProperty;
            }

        }

        public void IncludeTheseTypes(List<string> typeList)
        {
            foreach (string typeName in typeList)
                TypeInclusions.Add(typeName, true);
        }

        public void ExcludeTheseProperties(List<string> propertyList)
        {
            foreach (string propertyName in propertyList)
                PropertyInclusions.Add(propertyName, true);
        }

        protected bool IncludeThisType(Type aType)
        {
            if (TypeInclusions.ContainsKey(aType.Namespace + "." + aType.Name)) // generic type parameters don't have a full name - don't ask me why not...
                return TypeInclusions[aType.Namespace + "." + aType.Name];
            CIP4ItemAttribute att = CIP4ItemAttribute.GetAttribute(aType);
            if (att != null && !att.DontSerialize)
                TypeInclusions.Add(aType.Namespace + "." + aType.Name, true);
            else
                TypeInclusions.Add(aType.Namespace + "." + aType.Name, false);
            return TypeInclusions[aType.Namespace + "." + aType.Name];
        }

        protected bool VersionThisType(Type aType)
        {
            if (ValueType(aType))
                return true;
            Type actualType = GetActualType(aType);
            CIP4ItemAttribute att = CIP4ItemAttribute.GetAttribute(actualType);
            if (att != null && !att.DontSerialize)
            {
                return actualType.GetProperties().Any(t => t.PropertyType.Name == typeof(CIP4ItemVersionsType).Name);
            }
            return false;
        }

        internal bool IncludeThisProperty(PropertyInfo prop)
        {
            if (prop.DeclaringType.GetInterface("IList`1") != null && prop.Name == "Item")
                return false;
            if (PropertyInclusions.ContainsKey(prop.PropertyType.FullName + "." + prop.Name))
                return PropertyInclusions[prop.PropertyType.FullName + "." + prop.Name];
            CIP4PropertyAttribute att = CIP4PropertyAttribute.GetAttribute(prop);
            if (string.IsNullOrEmpty(prop.PropertyType.FullName))
                throw new ApplicationException("Unexpected null property type name");
            if (prop.PropertyType.Name == "CIP4ItemVersionsType")
                return false;
            if (att == null || !att.DontSerialize)
            {
                if (att != null && att.SerializeTarget)
                {
                    if (!TypeInclusions.ContainsKey(prop.PropertyType.FullName))
                        TypeInclusions.Add(prop.PropertyType.FullName, true);
                    else
                        TypeInclusions[prop.PropertyType.FullName] = true;
                }
                PropertyInclusions.Add(prop.PropertyType.FullName + "." + prop.Name, true);
            }
            else
                PropertyInclusions.Add(prop.PropertyType.FullName + "." + prop.Name, false);
            return PropertyInclusions[prop.PropertyType.FullName + "." + prop.Name];
        }

        protected IEnumerable<TypeContextType> TargetTypes(TypeContextType target)
        {
            if (!UsedTypes.ContainsKey(target.TheTypeFullName))
                yield return target;
            foreach (Type t in target.TheType.Assembly.GetTypes())
            {
                if (!target.TheType.IsAssignableFrom(t))
                    continue;
                TypeContextType tc = new TypeContextType { OriginalType = t };
                if (!UsedTypes.ContainsKey(tc.TheTypeFullName))
                    yield return tc;
            }
            yield break;
        }
        
        protected static List<PropertyInfo> GetExternalIDProperties(TypeContextType aType)
        {
            HashSet<int> rankSet = new HashSet<int> ();     // for detecting duplicate ranks
            List<PropertyInfo> result = new List<PropertyInfo>();
            foreach (PropertyInfo prop in WrapperSerializerType.SortedProperties(aType.TheType.GetProperties()).Values)
            {
                CIP4ExternalIDAttribute attribute = CIP4ExternalIDAttribute.GetAttribute (prop);
                if (attribute != null)
                {
                    if (attribute.Rank < 0)
                        throw new ArgumentException (string.Format ("Rank {0} is not valid: must be a non-negative integer.", attribute.Rank));

                    if (!rankSet.Add (attribute.Rank))
                        throw new ArgumentException (string.Format ("The rank value {0} appears more than once. This makes the sort order ambiguous.", attribute.Rank));

                    result.Add (prop);
                }
            }

            List<PropertyInfo> orderedProperties = new List<PropertyInfo>(result.OrderBy(x => CIP4ExternalIDAttribute.GetAttribute(x).Rank));

            List<Type> checkIfMixedUp = new List<Type>();

            foreach (PropertyInfo orderedProperty in orderedProperties)
                if (checkIfMixedUp.Contains(orderedProperty.PropertyType) && checkIfMixedUp[0] != orderedProperty.PropertyType)
                    throw new ArgumentException(string.Format("The rank of the properties in a given type must be contiguous - you cannot have t1.p1-rank=1, t2.p2-rank=2, t1.p3-rank=3 - see property {0} in type {1}", orderedProperty.Name, orderedProperty.PropertyType.Name));

            // Return a list sorted in ascending order by Rank...
            return orderedProperties;
        }

        internal static bool ValueType(Type type)
        {
            Type actualType = GetActualType(type);
            if (actualType.IsEnum)
                return true;
            if (actualType.Name == "String")
                return true;
            if (actualType.Name == "Color")
                return true;
            if (actualType.Name == "Uri")
                return true;
            if (actualType.Name == "TimeZoneInfo")
                return true;
            if (actualType.GetInterface("IEnumerable") != null)
                return false;
            return actualType.IsValueType;
        }

        private const string CSharpGetValueLiteral =
            // {0} - target type name
            // {1} - properties 
            // {2} - return external id if there is one if it's asked for
            // {3} - MakeExternalIDForGetValueGetSubtypes()
@"        public static object GetValueForPath(string[] path, int index, {0} item)
        {{
            if (item == null)
                return null;
{2}
{3}
            switch (path[index])
            {{
{1}
                default:
                    return null;
            }}

            // Not required as all previous paths get us out of here
            // return null;
        }}
";

        private const string CSharpForExternalIDForGetValueGetSubtypesLiteral = 
            // {0} - full subtype name
            // {1} - type serializer name
            @"            if (item is {0})
                return {1}.GetValueForPath(path, index, ({0})item);
";

        private const string CSharpGetExternalIDForGetValueLiteral =
            @"            if (index == 0)
                switch (path[0])
                {
                    case ""ExternalID"":
                        return GetExternalIDForObject(item);
                }

";

        private const string CSharpGetSimpleValueLiteral = 
@"                case ""{0}"":
                    return item.{0};
";

        private const string CSharpGetCompoundValueLiteral =
@"                case ""{0}"":
                    return CIP4.Serializer.{1}_Serializer.GetValueForPath(path, index + 1, item.{0});
";

        private const string CSharpMvUnsupportedLiteral =
@"                case ""{1}"":
                    throw new NotImplementedException(""{0} properties are not supported in expressions at this time: {1}"");
";

        protected string MakeGetValue()
        {
            string text = string.Empty;

            foreach (PropertyInfo property in WrapperSerializerType.SortedProperties(TheType.TheType.GetProperties()).Values)
            {
                CIP4ExternalIDAttribute extIDAtt = CIP4ExternalIDAttribute.GetAttribute(property);
            
                if (!IncludeThisProperty(property) && extIDAtt == null)
                    continue;
                if (property.PropertyType.GetInterface("IList`1") != null)
                    text += string.Format(CSharpMvUnsupportedLiteral, "IList", property.Name);
                else if (property.PropertyType.GetInterface("IDictionary") != null)
                    text += string.Format(CSharpMvUnsupportedLiteral, "Dictionary", property.Name);
                else
                    text += SerializeSV(property);
            }
            
            return string.Format(CSharpGetValueLiteral, 
                TheType.TheTypeFullName, 
                text, 
                GetExternalIDProperties(TheType).Count > 0? CSharpGetExternalIDForGetValueLiteral: string.Empty,
                MakeExternalIDForGetValueGetSubtypes());
        }

        private string MakeExternalIDForGetValueGetSubtypes()
        {
            string text = string.Empty;
            foreach (Type t in TheType.TheType.Assembly.GetTypes())
            {
                if (!TheType.TheType.IsAssignableFrom(t) || t == TheType.TheType)
                    continue;
                TypeContextType tc = new TypeContextType { OriginalType = t };
                text += string.Format(CSharpForExternalIDForGetValueGetSubtypesLiteral,
                    tc.TheTypeFullName,
                    tc.TypeSerializerName);
                if (!UsedTypes.ContainsKey(tc.TheTypeFullName))
                    UsedTypes.Add(tc.TheTypeFullName, tc);
            }
            return text;
        }

        private string SerializeSV(PropertyInfo property)
        {
            TypeContextType propType = new TypeContextType { OriginalType = property.PropertyType };
            if (propType.TheType == typeof(Stream))
                return string.Format(CSharpMvUnsupportedLiteral, "Stream", property.Name);
            if (propType.HostTypeName == "CIP4ReferenceType")
                return string.Format(CSharpMvUnsupportedLiteral, "CIP4ReferenceType", property.Name);
            if (propType.OriginalType.IsGenericType)
                return string.Format(CSharpMvUnsupportedLiteral, "Generic type", property.Name);
            if (propType.TheType == typeof(object))
                return string.Format(CSharpMvUnsupportedLiteral, "object", property.Name);
            if (ValueType(propType.TheType))
                return string.Format(CSharpGetSimpleValueLiteral, property.Name);
            if (IncludeThisType(property.PropertyType))
                return string.Format(CSharpGetCompoundValueLiteral, property.Name, property.PropertyType.Name);
            return string.Empty;
        }

        private const string CSharpGetKeysLiteral =
            @"        private static List<KeyValuePair<string, int>> _Keys;
        public static IEnumerable<KeyValuePair<string, int>> GetKeys()
        {{
            if (_Keys == null)
            {{
                _Keys = new List<KeyValuePair<string, int>>
                {{
{0}
                }};
            }}
            return _Keys;
        }}

";

        private const string CSharpMakeKeyLiteral = 
            // {0} - key property name
            // {1} - index number - 0 is the ExternalID index
            @"                    new KeyValuePair<string, int>(""{0}"", {1}),
";

        protected string MakeKeys()
        {
            string text = string.Empty;


            foreach (PropertyInfo prop in GetExternalIDProperties(TheType))
                text += string.Format(CSharpMakeKeyLiteral, prop.Name, 0);

            int index = 1;
            foreach (PropertyInfo prop in TheType.TheType.GetProperties())
            {
                CIP4ExternalIDAttribute extAtt = CIP4ExternalIDAttribute.GetAttribute(prop);
                if (extAtt != null)
                    continue;
                CIP4PropertyAttribute propAtt = CIP4PropertyAttribute.GetAttribute(prop);
                if (propAtt != null && propAtt.IsIndexed)
                    text += string.Format(CSharpMakeKeyLiteral, prop.Name, index++);
            }

            return string.Format(CSharpGetKeysLiteral, text);
        }
        
        private const string CSharpGetItemVersionBodyLiteral =
@"
            if (previousVersion == null || previousVersion.{0} == null || !previousVersion.{0}.Versions.ContainsKey(versionTimestamp))
                return previousVersion;
            return GetItemVersion(versionTimestamp, previousVersion, previousVersion.{0}.Versions[versionTimestamp].Values);
";

        protected string GetItemVersionBody()
        {
            if (VersionProperty == null)
                return "            return previousVersion;";

            return string.Format(CSharpGetItemVersionBodyLiteral,
                VersionProperty.Name);
        }


        // (0) - type name
        // (1) - version property name
        // {2} - ".Ticks" if the proeprty is a DateTime, string.Empty otherwise
        // (3) - either "version" if it's numeric or "new DateTime(version)" if it's a DateTime
        private const string CSharpForCheckVersionLiteral =
            @"
        public bool CheckVersion {{ get {{ return false; }} }}

        public static Int64 GetVersionNumber(object item)
        {{
            return (({0})item).{1}{2};
        }}

        public static void SetVersionNumber(object item, Int64 version)
        {{
            (({0})item).{1} = {3};
        }}
";

        protected string MakeCheckVersion()
        {
            foreach (PropertyInfo prop in TheType.TheType.GetProperties())
                if (CIP4ItemVersionAttribute.GetAttribute(prop) != null)
                {
                    if (!(prop.PropertyType == typeof(DateTime) || prop.PropertyType == typeof(Int64)))
                        throw new ArgumentException(string.Format("ItemVersion properties must be Int64 or DateTime: {0}.{1}", prop.DeclaringType.FullName, prop.Name));
                    string ticks = prop.PropertyType == typeof(DateTime) ? ".Ticks" : string.Empty;
                    string valueString = prop.PropertyType == typeof(DateTime) ? "new DateTime(version)" : "version";
                    return string.Format(CSharpForCheckVersionLiteral, TheType.TheTypeFullName, prop.Name, ticks, valueString);
                }
            return string.Empty;
        }


        private const string CSharpVersionSvLiteral =
@"
                if (value.{1} != null && value.{0} != (({2})value.{1}.GetCurrent<{2}>()).{0})
                    value.{1}.AddValue(""{0}"", (({2})value.{1}.GetCurrent<{2}>()).{0});
";

        private const string CSharpVersionSvNullableLiteral =
@"
                if (value.{1} != null && value.{0}.HasValue && value.{0} != (({2})value.{1}.GetCurrent<{2}>()).{0})
                    value.{1}.AddValue(""{0}"", (({2})value.{1}.GetCurrent<{2}>()).{0});
";

        protected string SerializeSimpleVersions(PropertyInfo property)
        {
            TypeContextType actualType = new TypeContextType { OriginalType = property.PropertyType };
            PropertyInfo versionsProp = VersionProperty;
            if (versionsProp != null)
            {
                if (actualType.HostTypeName == "Nullable")
                    return string.Format(CSharpVersionSvNullableLiteral,
                        property.Name,
                        versionsProp.Name,
                        TheType.TheTypeFullName);
                return string.Format(CSharpVersionSvLiteral,
                        property.Name,
                        versionsProp.Name,
                        TheType.TheTypeFullName);
            }
            return string.Empty;
        }

        // {0} - property name
        // {1} - qualified name of the type that owns the property
        // {2} - version property name
        // {3} - property type
        private const string CSharpEmptySvNullPropertyVersionLiteral =
            @"
            else if (value.{2} != null && value.{2}.GetCurrent<{1}>().{0} != default({3}))
            {{
                value.{2}.AddValue(""{0}"", value.{2}.GetCurrent<{1}>().{0});
            }}
";

        // {0} - property name
        // {1} - qualified name of the type that owns the property
        // {2} - version property name
        // {3} - property type
        private const string CSharpEmptySvNullNullablePropertyVersionLiteral =
            @"
            else if (value.{2} != null && value.{2}.GetCurrent<{1}>().{0} != null)
            {{
                value.{2}.AddValue(""{0}"", value.{2}.GetCurrent<{1}>().{0});
            }}
";

        protected string SerializeSVNullVersion(PropertyInfo property)
        {
            if (VersionProperty == null)
                return string.Empty;
            TypeContextType actualType = new TypeContextType(property.PropertyType);
            if (actualType.HostTypeName == "Nullable")
                return string.Format(CSharpEmptySvNullNullablePropertyVersionLiteral,
                    property.Name,
                    TheType.TheTypeFullName,
                    VersionProperty.Name);
            return string.Format(CSharpEmptySvNullPropertyVersionLiteral,
                property.Name,
                TheType.TheTypeFullName,
                VersionProperty.Name,
                actualType.TheTypeFullName);
        }


        protected static string GetIEqualityComparer(TypeContextType actualType)
        {
            if (actualType.TheType == typeof(string))
                return "StringComparer.CurrentCulture";
            if (ValueType(actualType.TheType))
                return string.Format("new EqualityComparer<{0}{1}>((x, y) => x == y, x => x.GetHashCode())", actualType.TheTypeFullName, actualType.IsNullable? "?": string.Empty);
            return string.Format("new {0}.EqualityComparer()", actualType.TypeSerializerName);
        }

    }
}
